Skip to content

feat(release): signed release bundles: build and attest, sign after verify (RFC-0046) - #208

Merged
Madraka merged 2 commits into
mainfrom
feat/release-bundles
Nov 12, 2026
Merged

Madraka merged 2 commits into
mainfrom
feat/release-bundles

Conversation

@Madraka

@Madraka Madraka commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

What

Signed release bundles, per RFC-0046 (status: Proposed). Every release can carry a deterministic, installable bundle of the standard project. A reusable workflow with no secrets builds and attests it, and a separate job in a protected environment signs it only after the attestation and the statement check pass. Hosts and tools can then deploy a release as it is, without building it themselves, and verify that what they install is byte-for-byte what this repository published.

Releases here are immutable, so the publish order changes in this PR whether or not bundles are on: tag → draft release → upload and re-check assets → npm → publish. v0.8.0 and older keep no bundle.

Changes

  • RFC-0046 (Proposed) and docs/security/release-signing-keys.md, which lists the active public key cloudhal-release-2026a.
  • core-contracts: release-manifest and release-statement schemas, with fixtures, generated TS validators and Rust types (crates/cloudhal-schema), and a changeset.
  • scripts/release-bundle.ts (+ scripts/release-bundle/): a deterministic builder. With a fixed SOURCE_DATE_EPOCH the same commit gives the same bytes. It writes cloudhal-release-<v>.tar.gz, manifest.json (per-part digests) and release-statement.json (version, commit, securityRelease, digests).
  • .github/workflows/bundle-build.yml (reusable): frozen lockfile, no Actions cache, no secrets. It attests the three files with actions/attest-build-provenance and uploads them unsigned.
  • .github/workflows/release.yml:
    • The new bundle and bundle-sign jobs run only when vars.RELEASE_BUNDLES == 'true' or in a dry run.
    • bundle-sign runs in environment release-signing, with no checkout and no package manager. It runs gh attestation verify (--signer-workflow …/bundle-build.yml --source-ref refs/heads/main --source-digest $GITHUB_SHA --deny-self-hosted-runners) and checks the statement, then signs it with RELEASE_SIGNING_KEY (raw Ed25519, release-statement.sig).
    • publish starts only if the run was not cancelled (!cancelled()).
    • A workflow_dispatch input dry_run uses a throwaway key and a draft that is always deleted, and publishes nothing.
  • scripts/publish-ci.ts: the immutable-release order above, resumable on "Re-run failed jobs". It reuses the tag (refusing one that points at another commit) and the draft, and uploads only missing or wrong assets. It also checks the signature against release.yml's pinned key, or against the dry run's own key.
  • scripts/release-publish-plan.ts, scripts/release-plan.ts: the dry run and security-release flags.
  • CONTRIBUTING.md / AGENTS.md: the release flow, the dry run (mandatory after any change to release.yml or publish-ci.ts) and recovery.

Before the first bundled release (maintainer actions, not in this PR)

  1. Create environment release-signing with a required reviewer and deployment branches limited to main, then add secret RELEASE_SIGNING_KEY. Do this before the first dispatch, otherwise a dry run auto-creates the environment unprotected.
  2. Run Actions › Release › Run workflow with dry_run: true. The new publish order has never run against GitHub, and the next release uses it even with bundles off.
  3. Set repository variable RELEASE_BUNDLES=true.
  4. Accept RFC-0046.

Deferred (designed in RFC-0046, not built): signed keys.json and yank list, the offline root key, KMS custody, @cloudhal/release-verify, and cloudhal deploy --release.

Verification

  • biome check (error level), size-check, font-fallbacks, mcp-tools-doc and demo-archive checks are clean. pnpm gen leaves every generated file unchanged.
  • turbo run typecheck: 20/20 tasks.
  • @cloudhal/cli 562 tests; @cloudhal/core-contracts 590 tests; cargo test -p cloudhal-schema --locked 21 tests; cargo clippy --all-targets -D warnings is clean.
  • actionlint is clean. Every action is pinned to a commit, and the tag→SHA mapping was checked for upload-artifact v7.0.2, download-artifact v8.0.2 and attest-build-provenance v4.2.2.
  • node scripts/publish-ci.ts --plan --dry-run prints the expected plan.
  • End to end: the builder's real output for the standard theme was signed by bundle-sign's dry-run branch with an in-memory key. The monorepo verifier accepts it with that key and refuses it with the release key, a flipped byte, the wrong commit, a securityRelease mismatch or the wrong key id. A downstream consumer's ingest (Clavius) accepts the same bundle and refuses each of those tamperings.
  • turbo run test --concurrency=1: 19/19 tasks pass. Under pnpm check's --concurrency=3, @cloudhal/mcp-server (not touched here) timed out at 30 s on the 8 GB development machine, while running alongside core-ts and studio. It passes 183/183 alone and in the serial run.

Checklist

  • pnpm check and cargo test --workspace pass locally (see the mcp-server note under Verification)
  • Contract change? pnpm gen was run and generated files are committed (docs/adr/0001)
  • User-visible change in a published package? pnpm changeset added
  • Commits are signed off (git commit -s, DCO)
  • Docs / AGENTS.md updated if behaviour or commands changed

🤖 Generated with Claude Code

Madraka and others added 2 commits November 12, 2026 10:55
…erify (RFC-0046)

- RFC-0046 (Proposed): a deterministic release bundle per release, built and attested in a
  reusable workflow with no secrets, signed by a separate job only after the attestation and
  the statement check pass
- core-contracts: release-manifest and release-statement schemas, fixtures, Rust types
- scripts/release-bundle.ts: deterministic builder; release-publish-plan.ts
- .github/workflows/bundle-build.yml (attest-build-provenance) and release.yml jobs bundle and
  bundle-sign (environment release-signing), gated on vars.RELEASE_BUNDLES, with a dry run
- publish-ci.ts: tag, draft release, upload and re-check, npm, then publish (immutable releases)
- docs/security/release-signing-keys.md lists the active public key cloudhal-release-2026a

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Madraka <[email protected]>
v0.8.0 was published (immutable, no assets) after this branch was cut, so the first
bundled release is the version after it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Madraka <[email protected]>
@Madraka
Madraka enabled auto-merge November 12, 2026 08:29
@Madraka
Madraka merged commit a674d69 into main Nov 12, 2026
9 checks passed
@Madraka
Madraka deleted the feat/release-bundles branch November 12, 2026 08:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant